package org.report.text;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Represents utility class for displaying text tables of arbitrary row and column numbers. Row height and column width
 * are calculated automatically depending on user data.
 * 
 * @author yevhenii.dermenzhii
 */
public class TextTable {
    private static final String NEW_LINE = "\n";
    private static final String EMTPY_TABLE_TEXT = "";
    private static final String INDENT = " ";

    /**
     * Header values
     */
    private String[] headers = new String[0];
    private List<String[]> rows = new ArrayList<String[]>();
    private TextTableStyle style = TextTableStyle.simpleTable();

    private TextColumn[] textColumns;
    private TextCell[][] matrix;

    private TextRow[] textRows;
    private TextRow headerRow;

    /**
     * Assigns column headers which will be displayed on top of the table. The number of header means the number of
     * table columns.
     * 
     * @param headers the headers to display on top
     * @return this
     */
    public TextTable setHeader(String... headers) {
        this.headers = headers;
        return this;
    }

    public TextTableStyle getStyle() {
        return style;
    }

    public void setStyle(TextTableStyle style) {
        this.style = style;
    }


    /**
     * Renders and returns string table representation.
     */
    public String getText() {
        assembleMatrix();
        if (matrix == null) return EMTPY_TABLE_TEXT;


        StringBuffer text = new StringBuffer();

        if (hasHeader()) {
            appendRowDelimeterWithLeftRightBorders(text);
            text.append(style.getLeftBorderWithIndent());
            String verticalDelimiter = "";
            for(TextColumn textColumn : textColumns) {
                text.append(verticalDelimiter);
                text.append(textColumn.getTextHeader());
                verticalDelimiter = style.getColumnDelimiterWithIndent();
            }
            text.append(style.getRightBorderWithIndent());
            text.append(NEW_LINE);
        }

        appendRowDelimeterWithLeftRightBorders(text);
        for(TextRow textRow : textRows) {
            for(int i = 0; i < textRow.height(); i++) {
                text.append(style.getLeftBorderWithIndent());
                String verticalDelimiter = "";
                
                for(TextCell cell : textRow.textCells) {
                    text.append(verticalDelimiter);
                    text.append(cell.getLine(i));
                    verticalDelimiter = style.getColumnDelimiterWithIndent();
                }
                
                text.append(style.getRightBorderWithIndent());
                text.append(NEW_LINE);
            }
            appendRowDelimeterWithLeftRightBorders(text);
        }

        return text.toString();
    }


    private void appendRowDelimeterWithLeftRightBorders(StringBuffer text) {
        if (style.getRowDelimiter() == 0)
            return;

        int width = style.getColumnIndent() * 2;
        for(TextColumn textColumn : textColumns)
            width += textColumn.width();
        width += (textColumns.length - 1) * stringLength(style.getColumnDelimiterWithIndent());
        char[] delimiters = new char[width];
        Arrays.fill(delimiters, style.getRowDelimiter());

        text.append(style.getLeftBorder());
        text.append(delimiters);
        text.append(style.getRightBorder());
        text.append(NEW_LINE);
    }

    private int stringLength(String string) {
        return string == null ? 0 : string.length();
    }

    private TextCell[][] assembleMatrix() {
        final int columnCount = calculateColumnCount();
        final int rowCount = calculateRowCount();

        headerRow = new TextRow();
        textColumns = new TextColumn[columnCount];
        textRows = new TextRow[rowCount];
        matrix = new TextCell[rowCount][columnCount];

        for(int i = 0; i < textColumns.length; i++) {
            String headerValue = i < headers.length ? headers[i] : style.getDefaultHeader();
            textColumns[i] = new TextColumn(this);
            TextCell textHeader = new TextCell(this, headerRow, textColumns[i], headerValue);
            textColumns[i].setHeader(textHeader);
            headerRow.add(textHeader);
        }

        for(int i = 0; i < rows.size(); i++) {
            TextRow textRow = new TextRow();
            textRows[i] = textRow;
        }

        for(int i = 0; i < rows.size(); i++) {
            String[] rowCells = rows.get(i);
            TextRow textRow = textRows[i];

            for(int j = 0; j < matrix[i].length; j++) {
                TextColumn textColumn = textColumns[j];
                matrix[i][j] = new TextCell(this, textRow, textColumn, rowCells[j]);
                textRow.add(matrix[i][j]);
                textColumn.add(matrix[i][j]);
            }
        }
        return matrix;
    }

    private boolean hasHeader() {
        return headers.length > 0;
    }

    /**
     * @return total number of rows
     */
    private int calculateRowCount() {
        return rows.size();
    }

    /**
     * Returns the maximum number of columns
     */
    private int calculateColumnCount() {
        int count = headers.length;
        for(String[] row : rows)
            count = Math.max(count, row.length);
        return count;
    }

    /**
     * Adds row cells. Rows are displayed in the order as they were added. If number of cells exceeds the number of
     * table headers, then redundant cells will be ignored.
     * @param cells the row cells
     * @return this
     */
    public TextTable addRow(String... cells) {
        rows.add(cells);
        return this;
    }

    static class TextColumn {
        private final TextTable table;
        private final List<TextCell> textCells = new ArrayList<TextCell>();
        private TextCell textHeader;
        private int width = -1;

        public TextColumn(TextTable table) {
            this.table = table;
        }

        public void setHeader(TextCell textHeader) {
            this.textHeader = textHeader;
        }

        public void add(TextCell textCell) {
            textCells.add(textCell);
        }

        public TextCell getTextHeader() {
            return textHeader;
        }

        public int width() {
            if (width == -1) {
                width = textHeader.valueWidth();
                for(TextCell textCell : textCells) {
                    width = Math.max(width, textCell.valueWidth());
                }
            }
            return width;
        }
    }

    static class TextRow {
        private final List<TextCell> textCells = new ArrayList<TextCell>();
        private int height = -1;

        public void add(TextCell textCell) {
            textCells.add(textCell);
        }

        public int height() {
            if (height == -1) {
                for(TextCell textCell : textCells) {
                    height = Math.max(height, textCell.valueHeight());
                }
            }
            return height;
        }
    }

    static class TextCell {
        private final List<String> lines = new ArrayList<String>();
        private final TextColumn column;
        private final TextRow row;
        private final TextTable table;

        TextCell(TextTable table, TextRow row, TextColumn column, Object value) {
            this.table = table;
            String strValue = value != null ? value.toString() : table.getStyle().getNullValue();
            String[] splitValue = strValue.split(NEW_LINE);
            for(String line : splitValue) {
                lines.add(line);
            }
            this.row = row;
            this.column = column;
        }

        int valueWidth() {
            int width = 0;
            for(String line : lines)
                width = Math.max(width, line.length());
            return width;
        }

        int valueHeight() {
            return lines.size();
        }

        @Override
        public String toString() {
            return getText();
        }

        public String getLine(int index) {
            if (index >= lines.size())
                return "";
            return String.format("%" + column.width() + "s", lines.get(index));
        }

        public String getText() {
            StringBuilder text = new StringBuilder();
            for(int i = 0; i < row.height(); i++) {
                if (i > 0)
                    text.append(NEW_LINE);
                text.append(getLine(i));
            }
            return text.toString();
        }
    }

    public static class TextTableStyle {
        private String columnDelimiter = INDENT;
        private String nullValue = "";
        private String defaultHeader = "";
        private int columnIndent = 0;
        private String columnIndentStr = "";
        private String leftBorder = "";
        private String rightBorder = "";
        private char rowDelimiter;

        public static TextTableStyle simpleTable() {
            TextTableStyle style = new TextTableStyle();
            return style;
        }

        public String getLeftBorderWithIndent() {
            StringBuffer indent = new StringBuffer();
            indent.append(getLeftBorder());
            indent.append(columnIndentStr);
            return indent.toString();
        }

        public String getRightBorderWithIndent() {
            StringBuffer indent = new StringBuffer();
            indent.append(columnIndentStr);
            indent.append(getRightBorder());
            return indent.toString();
        }

        public String getLeftBorder() {
            return leftBorder != null ? leftBorder : "";
        }

        public TextTableStyle setLeftBorder(String leftBorder) {
            this.leftBorder = leftBorder;
            return this;
        }

        public String getRightBorder() {
            return rightBorder != null ? rightBorder : "";
        }

        public TextTableStyle setRightBorder(String rightBorder) {
            this.rightBorder = rightBorder;
            return this;
        }

        public String getColumnDelimiter() {
            return columnDelimiter;
        }

        public TextTableStyle setColumnDelimiter(String columnDelimiter) {
            this.columnDelimiter = columnDelimiter;
            return this;
        }

        public String getNullValue() {
            return nullValue;
        }

        public TextTableStyle setNullValue(String nullValue) {
            this.nullValue = nullValue;
            return this;
        }

        public String getDefaultHeader() {
            return defaultHeader;
        }

        public TextTableStyle setDefaultHeader(String defaultHeader) {
            this.defaultHeader = defaultHeader;
            return this;
        }

        public TextTableStyle setColumnIndent(int columnIndent) {
            this.columnIndent = columnIndent;
            StringBuffer indent = new StringBuffer();
            for(int i = 0; i < columnIndent; i++)
                indent.append(INDENT);
            columnIndentStr = indent.toString();
            return this;
        }

        public int getColumnIndent() {
            return columnIndent;
        }

        public String getColumnDelimiterWithIndent() {
            if (columnDelimiter == null || columnDelimiter.isEmpty()) {
                return columnIndentStr;
            }
            else {
                return columnIndentStr + columnDelimiter + columnIndentStr;
            }
        }

        public TextTableStyle setRowDelimiter(char rowDelimiter) {
            this.rowDelimiter = rowDelimiter;
            return this;
        }

        public char getRowDelimiter() {
            return rowDelimiter;
        }
    }


}
